﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace AkzenteIT.Util.Asn1
{
    public class ObjectIdentifier
    {
        private readonly int value;

        static ObjectIdentifier()
        {
            KnownObjectIdentifierNames = new Dictionary<ObjectIdentifier, string>
                                         {
                                             {Parse("0"), "itu-t"},
                                             {Parse("1"), "iso"},
                                             {Parse("1.2"), "member-body"},
                                             {Parse("1.2.840"), "us"},
                                             {Parse("1.2.840.113549"), "rsadsi"},
                                             {Parse("1.2.840.113549.1"), "pkcs"},
                                             {Parse("1.2.840.113549.1.1"), "pkcs-1"},
                                             {Parse("1.2.840.113549.1.1.1"), "rsaEncryption"},
                                             {Parse("1.2.840.113549.1.1.11"), "sha256WithRSAEncryption"},
                                             {Parse("1.2.840.113549.1.2"), "pkcs-2"},
                                             {Parse("1.2.840.113549.1.3"), "pkcs-3"},
                                             {Parse("1.2.840.113549.1.4"), "pkcs-4"},
                                             {Parse("1.2.840.113549.1.5"), "pkcs-5"},
                                             {Parse("1.2.840.113549.1.6"), "pkcs-6"},
                                             {Parse("1.2.840.113549.1.7"), "pkcs-7"},
                                             {Parse("1.2.840.113549.1.7.0"), "module"},
                                             {Parse("1.2.840.113549.1.7.1"), "data"},
                                             {Parse("1.2.840.113549.1.7.2"), "signedData"},
                                             {Parse("1.2.840.113549.1.7.3"), "envelopedData"},
                                             {Parse("1.2.840.113549.1.7.4"), "signedAndEnvelopedData"},
                                             {Parse("1.2.840.113549.1.7.5"), "digestedData"},
                                             {Parse("1.2.840.113549.1.7.6"), "encryptedData"},
                                             {Parse("1.2.840.113549.1.7.7"), "dataWithAttributes"},
                                             {Parse("1.2.840.113549.1.7.8"), "encryptedPrivateKeyInfo"},
                                             {Parse("1.2.840.113549.1.8"), "pkcs-8"},
                                             {Parse("1.2.840.113549.1.9"), "pkcs-9"},
                                             {Parse("1.2.840.113549.1.9.1"), "emailAddress"},
                                             {Parse("1.2.840.113549.1.9.2"), "unstructuredName"},
                                             {Parse("1.2.840.113549.1.9.3"), "contentType"},
                                             {Parse("1.2.840.113549.1.9.4"), "messageDigest"},
                                             {Parse("1.2.840.113549.1.9.5"), "signingTime"},
                                             {Parse("1.2.840.113549.1.9.15"), "smimeCapabilities"},
                                             {Parse("1.2.840.113549.2"), "digestAlgorithm"},
                                             {Parse("1.2.840.113549.2.5"), "md5"},
                                             {Parse("1.2.840.113549.3"), "encryptionAlgorithm"},
                                             {Parse("1.2.840.113549.3.2"), "rc2-cbc"},
                                             {Parse("1.2.840.113549.3.7"), "des-ede3-cbc"},
                                             {Parse("1.3"), "identified-organization"},
                                             {Parse("1.3.14"), "oiw"},
                                             {Parse("1.3.14.3"), "secsig"},
                                             {Parse("1.3.14.3.2"), "algorithms"},
                                             {Parse("1.3.14.3.2.7"), "desCBC"},
                                             {Parse("2"), "joint-iso-itu-t"},
                                             {Parse("2.5"), "ds"},
                                             {Parse("2.5.4"), "attributeType"},
                                             {Parse("2.5.4.0"), "objectClass"},
                                             {Parse("2.5.4.3"), "commonName"},
                                             {Parse("2.5.4.4"), "surname"},
                                             {Parse("2.5.4.6"), "countryName"},
                                             {Parse("2.5.4.7"), "localityName"},
                                             {Parse("2.5.4.10"), "organizationName"},
                                             {Parse("2.5.29"), "certificateExtension"},
                                             {Parse("2.5.29.14"), "subjectKeyIdentifier"},
                                             {Parse("2.5.29.15"), "keyUsage"},
                                             {Parse("2.5.29.19"), "basicConstraints"},
                                             {Parse("2.5.29.31"), "cRLDistributionPoints"},
                                             {Parse("2.5.29.35"), "authorityKeyIdentifier"},
                                             {Parse("2.16"), "country"},
                                             {Parse("2.16.840"), "us"},
                                             {Parse("2.16.840.1"), "organization"},
                                             {Parse("2.16.840.1.101"), "gov"},
                                             {Parse("2.16.840.1.101.3"), "csor"},
                                             {Parse("2.16.840.1.101.3.4"), "nistAlgorithm"},
                                             {Parse("2.16.840.1.101.3.4.2"), "hashAlgs"},
                                             {Parse("2.16.840.1.101.3.4.2.1"), "sha256"},
                                         };
        }

        public ObjectIdentifier(int[] parts) : this(parts, parts.Length - 1)
        {
        }

        private ObjectIdentifier(int[] parts, int pos)
        {
            value = parts[pos];

            if (pos > 0)
            {
                Parent = new ObjectIdentifier(parts, pos - 1);
            }
        }

        public ObjectIdentifier Parent { get; private set; }
        public static IDictionary<ObjectIdentifier, string> KnownObjectIdentifierNames { get; private set; }

        public static ObjectIdentifier Parse(string oid)
        {
            // TODO: Improve
            string[] parts = oid.Split(new[] {'{', '}', ' ', '/', '.'}, StringSplitOptions.RemoveEmptyEntries);

            return new ObjectIdentifier(parts.Select(p => int.Parse(p)).ToArray());
        }

        public static ObjectIdentifier ParseEncoded(byte[] oid)
        {
            var oidValues = new List<int>();

            // first byte
            if (oid.Length > 0)
            {
                oidValues.Add(oid[0]/40);
                oidValues.Add(oid[0]%40);
            }

            int current = 0;
            for (int i = 1; i < oid.Length; i++)
            {
                current = (current << 7) | oid[i] & 0x7F;

                //check if last byte
                if ((oid[i] & 0x80) == 0)
                {
                    oidValues.Add(current);
                    current = 0;
                }
            }

            return new ObjectIdentifier(oidValues.ToArray());
        }

        /// <summary>
        /// Returns true if this instance is a descendant of the given ObjectIdentifier
        /// </summary>
        /// <param name="other">The ObjectIdentifier for which the test will be performed</param>
        /// <returns>True, if this instance is a descendant of the given ObjectIdentifier, otherwise false</returns>
        public bool IsDescendantOf(ObjectIdentifier other)
        {
            var current = this;

            do
            {
                if (current == other)
                {
                    return true;
                }

                current = current.Parent;
            } while (current != null);

            return false;
        }

        public override string ToString()
        {
            return ToNamedString(name => null);
        }

        public string ToNamedString()
        {
            return ToNamedString(KnownObjectIdentifierNames);
        }

        public string ToNamedString(IDictionary<ObjectIdentifier, string> nameLookup)
        {
            return ToNamedString(name => nameLookup.ContainsKey(name) ? nameLookup[name] : null);
        }

        public string ToNamedString(Converter<ObjectIdentifier, string> nameLookup)
        {
            return "{" + ToNamedStringInternal(nameLookup) + "}";
        }

        private string ToNamedStringInternal(Converter<ObjectIdentifier, string> nameLookup)
        {
            string parentString = "";
            if (Parent != null)
            {
                parentString = Parent.ToNamedStringInternal(nameLookup) + " ";
            }

            string valueString = nameLookup(this);

            if (valueString == null)
            {
                valueString = value.ToString(CultureInfo.InvariantCulture);
            }
            else
            {
                valueString = string.Format(CultureInfo.InvariantCulture, "{0}({1})", valueString, value);
            }

            return parentString + valueString;
        }

        protected bool Equals(ObjectIdentifier other)
        {
            return value == other.value && Equals(Parent, other.Parent);
        }

        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) return false;
            if (ReferenceEquals(this, obj)) return true;
            if (obj.GetType() != GetType()) return false;
            return Equals((ObjectIdentifier) obj);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                return (value*397) ^ (Parent != null ? Parent.GetHashCode() : 0);
            }
        }

        public static bool operator ==(ObjectIdentifier left, ObjectIdentifier right)
        {
            return Equals(left, right);
        }

        public static bool operator !=(ObjectIdentifier left, ObjectIdentifier right)
        {
            return !Equals(left, right);
        }
    }
}